#include "guish.hpp"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <algorithm>

#include <grp.h>
#include <pwd.h>

static FileEntryList *listDirectory(string path, list<string> fields, bool showHidden);
static void sendDirectoryList(const FileEntryList *entries, StreamSender *send);

string fieldColumnWidths(const FileEntryList *entries);
string multiColumnWidths(const FileEntryList *entries);
string multiColumnWidths(const vector<int> &lengths);

string uidToUserName(uid_t uid);
string gidToGroupName(gid_t gid);

static bool compareFiles(const FileEntry &lhs, const FileEntry &rhs)
	{ return lhs < rhs; }

/////////////////////////////////////////////////////////////////////////////

BUILTIN(ls, "ls")
{
	if(!job->getState()->isInteractive)
		throw DisabledBuiltin();
	
	char workingDir[1024];
	getcwd(workingDir, 1024);
	
	LsCommand command(argv, string(workingDir));
	return doLs(workingDir, command, job, job->jobid, true);
}

RPC_HANDLER(ls)
{
	// TODO: Run this in its own thread
	int id = params->getInt("id");
	LsCommand cmd = LsCommand(params->getString("cmd"));
	
	ShellState *state = shell->getState();
	string pwd = state->env.get("PWD");
	doLs(pwd.c_str(), cmd.toCommand(), NULL, id, false);
}

int doLs(const char *baseDir, LsCommand command, Job *job, int id, bool unsolicited)
{
	try {
		string commandString = command.toCommand();
		
		string path = command.getPath();
		list<string> fields;
		command.getFieldsNeeded(fields);
		
		FileEntryList *entries = listDirectory(path, fields, command.getShowHidden());
		string columnWidths;
		
		// Calculate column widths to send. This is because the client needs
		// to lay out the columns before the full file list has been
		// downloaded. Column sizes are given in monospace characters, and
		// don't include icons, margins, and inter-column spacing.
		if(command.getIsDetailed())
		{
			// For a list view, the column widths are equal to the length of
			// the longest string in each field.
			columnWidths = fieldColumnWidths(entries);
		}
		else
		{
			// If this is a column view, calculate and send the total of all
			// column widths for each layout from 2 to LS_MAX_COLUMNS columns.
			columnWidths = multiColumnWidths(entries);
		}
		
		if(unsolicited)
			job->startClientWidget();
		
		RPCParams messageParams;
			if(job)
				messageParams.set("job", job->jobid);
			else
				messageParams.set("id", id);
			messageParams.set("cmd", commandString);
			messageParams.set("path", path);
			messageParams.set("fields", stringListToString(fields));
			messageParams.set("cols", columnWidths);
			messageParams.set("files", (int)entries->size());
			StreamSender *sender = new StreamSender(clientConnection, &messageParams);
		if(unsolicited)
			clientConnection->call("beginls", &messageParams);
		else
			clientConnection->call("dir", &messageParams);
		
		sendDirectoryList(entries, sender);
		delete entries;
		
		if(unsolicited)
			job->endClientWidget();
		
		delete sender;
	}
	catch(MessageException err)
	{
		string errorMessage = string("ls: ")+err.toString();
		job->reportError(errorMessage.c_str());
	}
	return 0;
}

/////////////////////////////////////////////////////////////////////////////

string fieldColumnWidths(const FileEntryList *entries)
{
	if(!entries || !entries->size())
		return "";
	
	// Compute the lengths of the longest field in each position
	vector<int> lengths;
	for(list<string>::const_iterator ii=(*entries)[0].fields.begin(); ii!=(*entries)[0].fields.end(); ii++)
		lengths.push_back(ii->length());
	
	for(size_t ii=1; ii<entries->size(); ii++)
	{
		const list<string> &fields = (*entries)[ii].fields;
		int fieldIndex = 0;
		for(list<string>::const_iterator jj=fields.begin(); jj!=fields.end(); jj++, fieldIndex++)
		{
			int fieldLen = jj->length();
			if(fieldLen > lengths[fieldIndex])
				lengths[fieldIndex] = fieldLen;
		}
	}
	
	// Convert it into a space-separated string
	string ret = "";
	for(size_t ii=0; ii<lengths.size(); ii++)
		ret += retprintf( (ii?" %i":"%i"), lengths[ii] );
	return ret;
}

string multiColumnWidths(const FileEntryList *entries)
{
	vector<int> lengths;
	
	for(size_t ii=0; ii<entries->size(); ii++)
		lengths.push_back( (*entries)[ii].name.length() );
	
	return multiColumnWidths(lengths);
}

string multiColumnWidths(const vector<int> &lengths)
{
	string ret = "";
	for(int columns=2; columns<=LS_MAX_COLUMNS; columns++)
	{
		vector<int> columnWidths;
		int totalWidth = 0;
		int rows = (lengths.size() + (columns-1)) / columns;
		
		int currentWidth = 0;
		int row = 0;
		for(size_t ii=0; ii<lengths.size(); ii++)
		{
			if(lengths[ii] > currentWidth)
				currentWidth = lengths[ii];
			row++;
			
			if(row>=rows || ii+1==lengths.size()) {
				row = 0;
				totalWidth += currentWidth;
				currentWidth = 0;
			}
		}
		
		ret += retprintf((columns==2)?"%i":" %i", totalWidth);
	}
	return ret;
}

TEST_SET(multiColumnWidths)
{
	int nameLengths[] = { 1,5,2,4,3,9,1,3,4,5 };
	vector<int> vec;
	for(int ii=0; ii<sizeof(nameLengths)/sizeof(int); ii++)
		vec.push_back(nameLengths[ii]);
	
	testAssert(multiColumnWidths(vec) == "14 19 23 26 26");
}

/////////////////////////////////////////////////////////////////////////////
typedef string (*FieldFiller)(const char *fileName, struct stat *fileStat);

string fieldName(const char *fileName, struct stat *fileStat)
{
	return fileName;
}

string fieldSize(const char *fileName, struct stat *fileStat)
{
	char buf[32];
	sprintf(buf, "%lli", (long long int)fileStat->st_size);
	return buf;
}

string fieldType(const char *fileName, struct stat *fileStat)
{
	if(S_ISREG(fileStat->st_mode))
		return "reg";
	else if(S_ISDIR(fileStat->st_mode))
		return "dir";
#ifdef S_ISCHR
	else if(S_ISCHR(fileStat->st_mode))
		return "chr";
#endif
#ifdef S_ISBLK
	else if(S_ISBLK(fileStat->st_mode))
		return "blk";
#endif
#ifdef S_ISFIFO
	else if(S_ISFIFO(fileStat->st_mode))
		return "fifo";
#endif
#ifdef S_ISLNK
	else if(S_ISLNK(fileStat->st_mode))
		return "lnk";
#endif
#ifdef S_ISSOCK
	else if(S_ISSOCK(fileStat->st_mode))
		return "sock";
#endif
	else
		return "other";
}

string fieldLinks(const char *fileName, struct stat *fileStat)
{
	char buf[32];
	sprintf(buf, "%i", (int)fileStat->st_nlink);
	return buf;
}

string fieldAtime(const char *fileName, struct stat *fileStat)
{
	return timeToString(fileStat->st_atime);
}

string fieldMtime(const char *fileName, struct stat *fileStat)
{
	return timeToString(fileStat->st_mtime);
}

string fieldCtime(const char *fileName, struct stat *fileStat)
{
	return timeToString(fileStat->st_ctime);
}

string fieldPermissions(const char *fileName, struct stat *fileStat)
{
	string ret = "----------";
	
	if(fileStat->st_mode & S_IFDIR) ret[0]='d';
	
	if(fileStat->st_mode & S_IRUSR) ret[1]='r';
	if(fileStat->st_mode & S_IWUSR) ret[2]='w';
	if(fileStat->st_mode & S_IXUSR) ret[3]='x';
	
	if(fileStat->st_mode & S_IRGRP) ret[4]='r';
	if(fileStat->st_mode & S_IWGRP) ret[5]='w';
	if(fileStat->st_mode & S_IXGRP) ret[6]='x';
	
	if(fileStat->st_mode & S_IROTH) ret[7]='r';
	if(fileStat->st_mode & S_IWOTH) ret[8]='w';
	if(fileStat->st_mode & S_IXOTH) ret[9]='x';
	
	if(fileStat->st_mode & S_ISGID) ret[3]=ret[6]='s';
	if(fileStat->st_mode & S_ISUID) ret[3]=ret[6]='S';
	
	if(fileStat->st_mode & S_ISVTX) ret[9]='T';
	
	return ret;
}

string fieldOwner(const char *fileName, struct stat *fileStat)
{
	return uidToUserName(fileStat->st_uid);
}

string fieldGroup(const char *fileName, struct stat *fileStat)
{
	return gidToGroupName(fileStat->st_gid);
}

string uidToUserName(uid_t uid)
{
	struct passwd *pw = getpwuid(uid);
	if(pw)
		return string(pw->pw_name);
	else
		return "?????";
}

string gidToGroupName(gid_t gid)
{
	struct group *group = getgrgid(gid);
	if(group)
		return string(group->gr_name);
	else
		return "?????";
}

string fieldNull(const char *fileName, struct stat *fileStat)
{
	return "";
}

FieldFiller getFieldFiller(string name)
{
	if(name=="name")
		return fieldName;
	else if(name=="size")
		return fieldSize;
	else if(name=="type")
		return fieldType;
	else if(name=="links")
		return fieldLinks;
	else if(name=="atime")
		return fieldAtime;
	else if(name=="mtime")
		return fieldMtime;
	else if(name=="ctime")
		return fieldCtime;
	else if(name=="perm")
		return fieldPermissions;
	else if(name=="owner")
		return fieldOwner;
	else if(name=="group")
		return fieldGroup;
	else
		return fieldNull;
}

/////////////////////////////////////////////////////////////////////////////

static FileEntryList *listDirectory(string path, list<string> fields, bool showHidden)
{
	FileEntryList *entries = new FileEntryList();
	
	// This request comes with a list of fields (size, file type, creation
	// time, etc) which should be included in the response. For each such
	// field, we use a callback function that takes what we know about the file
	// and fills in that field.
	vector<FieldFiller> fieldFillers;
	for(list<string>::iterator ii=fields.begin(); ii!=fields.end(); ii++)
		fieldFillers.push_back(getFieldFiller(*ii));
	
	DIR *dir = opendir(path.c_str());
	if(!dir) {
		return entries;
	}
	
	dirent *dirEntry;
	struct stat stat_buf;
	
	// Special case for the one absolute path which ends in a slash
	if(path=="/")
		path = "";
	
	while((dirEntry = readdir(dir)))
	{
		if(dirEntry->d_name[0] == '.' && !showHidden)
			continue;
		
		FileEntry entry;
		entry.name = dirEntry->d_name;
		
		string fullPath = path + "/" + entry.name;
		lstat(fullPath.c_str(), &stat_buf);
		
		for(size_t ii=0; ii<fieldFillers.size(); ii++)
			entry.fields.push_back(fieldFillers[ii](entry.name.c_str(), &stat_buf));
		
		entries->push_back(entry);
	}
	
	std::sort(entries->begin(), entries->end(), compareFiles);
	closedir(dir);
	return entries;
}

void sendDirectoryList(const FileEntryList *entries, StreamSender *stream)
{
	for(size_t ii=0; ii<entries->size(); ii++)
	{
		string line = stringListToString((*entries)[ii].fields) + "\n";
		stream->send(line.length(), line.c_str());
	}
}

